import { assert, beforeEach, describe, expect, inject, it } from "vitest";

import { createKarakeepClient } from "@karakeep/sdk";

import { createTestUser } from "../../utils/api";
import { waitUntil } from "../../utils/general";

describe("Bookmarks API", () => {
  const port = inject("karakeepPort");

  if (!port) {
    throw new Error("Missing required environment variables");
  }

  let client: ReturnType<typeof createKarakeepClient>;
  let apiKey: string;

  beforeEach(async () => {
    apiKey = await createTestUser();
    client = createKarakeepClient({
      baseUrl: `http://localhost:${port}/api/v1/`,
      headers: {
        "Content-Type": "application/json",
        authorization: `Bearer ${apiKey}`,
      },
    });
  });

  it("should create and retrieve a bookmark", async () => {
    // Create a new bookmark
    const {
      data: createdBookmark,
      response: createResponse,
      error,
    } = await client.POST("/bookmarks", {
      body: {
        type: "text",
        title: "Test Bookmark",
        text: "This is a test bookmark",
      },
    });

    if (error) {
      console.error("Error creating bookmark:", error);
      throw error;
    }

    expect(createResponse.status).toBe(201);
    expect(createdBookmark).toBeDefined();
    expect(createdBookmark?.id).toBeDefined();

    // Get the created bookmark
    const { data: retrievedBookmark, response: getResponse } = await client.GET(
      "/bookmarks/{bookmarkId}",
      {
        params: {
          path: {
            bookmarkId: createdBookmark.id,
          },
        },
      },
    );

    expect(getResponse.status).toBe(200);
    expect(retrievedBookmark!.id).toBe(createdBookmark.id);
    expect(retrievedBookmark!.title).toBe("Test Bookmark");
    assert(retrievedBookmark!.content.type === "text");
    expect(retrievedBookmark!.content.text).toBe("This is a test bookmark");
  });

  it("should update a bookmark", async () => {
    // Create a new bookmark
    const { data: createdBookmark, error: createError } = await client.POST(
      "/bookmarks",
      {
        body: {
          type: "text",
          title: "Test Bookmark",
          text: "This is a test bookmark",
        },
      },
    );

    if (createError) {
      console.error("Error creating bookmark:", createError);
      throw createError;
    }
    if (!createdBookmark) {
      throw new Error("Bookmark creation failed");
    }

    // Update the bookmark
    const { data: updatedBookmark, response: updateResponse } =
      await client.PATCH("/bookmarks/{bookmarkId}", {
        params: {
          path: {
            bookmarkId: createdBookmark.id,
          },
        },
        body: {
          title: "Updated Title",
        },
      });

    expect(updateResponse.status).toBe(200);
    expect(updatedBookmark!.title).toBe("Updated Title");
  });

  it("should delete a bookmark", async () => {
    // Create a new bookmark
    const { data: createdBookmark, error: createError } = await client.POST(
      "/bookmarks",
      {
        body: {
          type: "text",
          title: "Test Bookmark",
          text: "This is a test bookmark",
        },
      },
    );

    if (createError) {
      console.error("Error creating bookmark:", createError);
      throw createError;
    }
    if (!createdBookmark) {
      throw new Error("Bookmark creation failed");
    }

    // Delete the bookmark
    const { response: deleteResponse } = await client.DELETE(
      "/bookmarks/{bookmarkId}",
      {
        params: {
          path: {
            bookmarkId: createdBookmark.id,
          },
        },
      },
    );

    expect(deleteResponse.status).toBe(204);

    // Verify it's deleted
    const { response: getResponse } = await client.GET(
      "/bookmarks/{bookmarkId}",
      {
        params: {
          path: {
            bookmarkId: createdBookmark.id,
          },
        },
      },
    );

    expect(getResponse.status).toBe(404);
  });

  it("should paginate through bookmarks", async () => {
    // Create multiple bookmarks
    const bookmarkPromises = Array.from({ length: 5 }, (_, i) =>
      client.POST("/bookmarks", {
        body: {
          type: "text",
          title: `Test Bookmark ${i}`,
          text: `This is test bookmark ${i}`,
        },
      }),
    );

    const createdBookmarks = await Promise.all(bookmarkPromises);
    const bookmarkIds = createdBookmarks.map((b) => b.data!.id);

    // Get first page
    const { data: firstPage, response: firstResponse } = await client.GET(
      "/bookmarks",
      {
        params: {
          query: {
            limit: 2,
          },
        },
      },
    );

    expect(firstResponse.status).toBe(200);
    expect(firstPage!.bookmarks.length).toBe(2);
    expect(firstPage!.nextCursor).toBeDefined();

    // Get second page
    const { data: secondPage, response: secondResponse } = await client.GET(
      "/bookmarks",
      {
        params: {
          query: {
            limit: 2,
            cursor: firstPage!.nextCursor!,
          },
        },
      },
    );

    expect(secondResponse.status).toBe(200);
    expect(secondPage!.bookmarks.length).toBe(2);
    expect(secondPage!.nextCursor).toBeDefined();

    // Get final page
    const { data: finalPage, response: finalResponse } = await client.GET(
      "/bookmarks",
      {
        params: {
          query: {
            limit: 2,
            cursor: secondPage!.nextCursor!,
          },
        },
      },
    );

    expect(finalResponse.status).toBe(200);
    expect(finalPage!.bookmarks.length).toBe(1);
    expect(finalPage!.nextCursor).toBeNull();

    // Verify all bookmarks were returned
    const allBookmarks = [
      ...firstPage!.bookmarks,
      ...secondPage!.bookmarks,
      ...finalPage!.bookmarks,
    ];
    expect(allBookmarks.map((b) => b.id)).toEqual(
      expect.arrayContaining(bookmarkIds),
    );
  });

  it("should manage tags on a bookmark", async () => {
    // Create a new bookmark
    const { data: createdBookmark, error: createError } = await client.POST(
      "/bookmarks",
      {
        body: {
          type: "text",
          title: "Test Bookmark",
          text: "This is a test bookmark",
        },
      },
    );

    if (createError) {
      console.error("Error creating bookmark:", createError);
      throw createError;
    }
    if (!createdBookmark) {
      throw new Error("Bookmark creation failed");
    }

    // Add tags
    const { data: addTagsResponse, response: addTagsRes } = await client.POST(
      "/bookmarks/{bookmarkId}/tags",
      {
        params: {
          path: {
            bookmarkId: createdBookmark.id,
          },
        },
        body: {
          tags: [{ tagName: "test-tag" }],
        },
      },
    );

    expect(addTagsRes.status).toBe(200);
    expect(addTagsResponse!.attached.length).toBe(1);

    // Remove tags
    const { response: removeTagsRes } = await client.DELETE(
      "/bookmarks/{bookmarkId}/tags",
      {
        params: {
          path: {
            bookmarkId: createdBookmark.id,
          },
        },
        body: {
          tags: [{ tagId: addTagsResponse!.attached[0] }],
        },
      },
    );

    expect(removeTagsRes.status).toBe(200);
  });

  it("should get lists for a bookmark", async () => {
    const { data: createdBookmark } = await client.POST("/bookmarks", {
      body: {
        type: "text",
        title: "Test Bookmark",
        text: "This is a test bookmark",
      },
    });

    const { data: createdList } = await client.POST("/lists", {
      body: {
        name: "Test List",
        icon: "📚",
      },
    });

    const { response: addBookmarkResponse } = await client.PUT(
      "/lists/{listId}/bookmarks/{bookmarkId}",
      {
        params: {
          path: {
            listId: createdList!.id,
            bookmarkId: createdBookmark!.id,
          },
        },
      },
    );

    expect(addBookmarkResponse.status).toBe(204);

    const { data: lists, response: getListsResponse } = await client.GET(
      "/bookmarks/{bookmarkId}/lists",
      {
        params: {
          path: {
            bookmarkId: createdBookmark!.id,
          },
        },
      },
    );

    expect(getListsResponse.status).toBe(200);
    expect(lists!.lists.length).toBe(1);
    expect(lists!.lists[0].id).toBe(createdList!.id);
    expect(lists!.lists[0].name).toBe("Test List");
    expect(lists!.lists[0].icon).toBe("📚");
  });

  it("should search bookmarks", async () => {
    // Create test bookmarks
    await client.POST("/bookmarks", {
      body: {
        type: "text",
        title: "Search Test 1",
        text: "This is a test bookmark for search",
      },
    });
    await client.POST("/bookmarks", {
      body: {
        type: "text",
        title: "Search Test 2",
        text: "Another test bookmark for search",
      },
    });

    await waitUntil(async () => {
      const { data, response, error } = await client.GET("/bookmarks/search", {
        params: {
          query: {
            q: "test bookmark",
          },
        },
      });
      if (error) {
        throw error;
      }
      if (response.status !== 200) {
        throw new Error(`Search request failed with status ${response.status}`);
      }
      return (data?.bookmarks.length ?? 0) >= 2;
    }, 'Search index contains the new bookmarks for query "test bookmark"');

    // Search for bookmarks
    const { data: searchResults, response: searchResponse } = await client.GET(
      "/bookmarks/search",
      {
        params: {
          query: {
            q: "test bookmark",
          },
        },
      },
    );

    expect(searchResponse.status).toBe(200);
    expect(searchResults!.bookmarks.length).toBeGreaterThanOrEqual(2);
  });

  it("should paginate search results", async () => {
    // Create multiple bookmarks
    const bookmarkPromises = Array.from({ length: 5 }, (_, i) =>
      client.POST("/bookmarks", {
        body: {
          type: "text",
          title: `Search Pagination ${i}`,
          text: `This is test bookmark ${i} for pagination`,
        },
      }),
    );

    await Promise.all(bookmarkPromises);

    await waitUntil(async () => {
      const { data, response, error } = await client.GET("/bookmarks/search", {
        params: {
          query: {
            q: "pagination",
            limit: 5,
          },
        },
      });
      if (error) {
        throw error;
      }
      if (response.status !== 200) {
        throw new Error(`Search request failed with status ${response.status}`);
      }
      return (data?.bookmarks.length ?? 0) >= 5;
    }, "Search index contains the pagination test bookmarks");

    // Get first page
    const { data: firstPage, response: firstResponse } = await client.GET(
      "/bookmarks/search",
      {
        params: {
          query: {
            q: "pagination",
            limit: 2,
          },
        },
      },
    );

    expect(firstResponse.status).toBe(200);
    expect(firstPage!.bookmarks.length).toBe(2);
    expect(firstPage!.nextCursor).toBeDefined();

    // Get second page
    const { data: secondPage, response: secondResponse } = await client.GET(
      "/bookmarks/search",
      {
        params: {
          query: {
            q: "pagination",
            limit: 2,
            cursor: firstPage!.nextCursor!,
          },
        },
      },
    );

    expect(secondResponse.status).toBe(200);
    expect(secondPage!.bookmarks.length).toBe(2);
    expect(secondPage!.nextCursor).toBeDefined();

    // Get final page
    const { data: finalPage, response: finalResponse } = await client.GET(
      "/bookmarks/search",
      {
        params: {
          query: {
            q: "pagination",
            limit: 2,
            cursor: secondPage!.nextCursor!,
          },
        },
      },
    );

    expect(finalResponse.status).toBe(200);
    expect(finalPage!.bookmarks.length).toBe(1);
    expect(finalPage!.nextCursor).toBeNull();
  });

  describe("singlefile", () => {
    async function uploadSinglefileAsset(ifexists?: string) {
      const file = new File(["<html>HELLO WORLD</html>"], "test.html", {
        type: "text/html",
      });

      const formData = new FormData();
      formData.append("url", "https://example.com");
      formData.append("file", file);

      const url = new URL(
        `http://localhost:${port}/api/v1/bookmarks/singlefile`,
      );
      if (ifexists) {
        url.searchParams.append("ifexists", ifexists);
      }

      const response = await fetch(url.toString(), {
        method: "POST",
        headers: {
          authorization: `Bearer ${apiKey}`,
        },
        body: formData,
      });

      if (!response.ok) {
        return [null, response] as const;
      }

      const data = (await response.json()) as { id: string };
      return [data, response] as const;
    }

    it("should support precrawling via singlefile with ifexists=skip", async () => {
      // First upload: create a bookmark
      const [data, response] = await uploadSinglefileAsset();
      expect(response?.status).toBe(201);
      const bookmarkId = data?.id;
      if (!bookmarkId) throw new Error("Bookmark ID not found");

      // Get the bookmark and record the precrawled asset id
      const { data: bookmark, response: getResponse1 } = await client.GET(
        "/bookmarks/{bookmarkId}",
        {
          params: { path: { bookmarkId } },
        },
      );
      expect(getResponse1.status).toBe(200);
      const assetIds = bookmark!.assets
        .filter((a) => a.assetType === "precrawledArchive")
        .map((a) => a.id);
      expect(assetIds.length).toBe(1);
      const firstAssetId = assetIds[0];

      // Second upload with skip
      const [data2, response2] = await uploadSinglefileAsset("skip");
      expect(response2?.status).toBe(200);
      expect(data2?.id).toBe(bookmarkId);

      // Get the bookmark again
      const { data: bookmark2, response: getResponse2 } = await client.GET(
        "/bookmarks/{bookmarkId}",
        {
          params: { path: { bookmarkId } },
        },
      );
      expect(getResponse2.status).toBe(200);
      const assetIds2 = bookmark2!.assets
        .filter((a) => a.assetType === "precrawledArchive")
        .map((a) => a.id);
      expect(assetIds2).toEqual([firstAssetId]); // same asset
    });

    it("should support precrawling via singlefile with ifexists=overwrite", async () => {
      // First upload
      const [data, response] = await uploadSinglefileAsset("overwrite");
      expect(response?.status).toBe(201);
      const bookmarkId = data?.id;
      if (!bookmarkId) throw new Error("Bookmark ID not found");

      // Record the asset
      const { data: bookmark, response: getResponse1 } = await client.GET(
        "/bookmarks/{bookmarkId}",
        {
          params: { path: { bookmarkId } },
        },
      );
      expect(getResponse1.status).toBe(200);
      const firstAssetId = bookmark!.assets.find(
        (a) => a.assetType === "precrawledArchive",
      )?.id;
      expect(firstAssetId).toBeDefined();

      // Second upload with overwrite
      const [data2, response2] = await uploadSinglefileAsset("overwrite");
      expect(response2?.status).toBe(200);
      expect(data2?.id).toBe(bookmarkId);

      // Get the bookmark again
      const { data: bookmark2, response: getResponse2 } = await client.GET(
        "/bookmarks/{bookmarkId}",
        {
          params: { path: { bookmarkId } },
        },
      );
      expect(getResponse2.status).toBe(200);
      const secondAssetId = bookmark2!.assets.find(
        (a) => a.assetType === "precrawledArchive",
      )?.id;
      expect(secondAssetId).toBeDefined();
      expect(secondAssetId).not.toBe(firstAssetId);
      // There should be only one precrawledArchive asset
      const precrawledAssets = bookmark2!.assets.filter(
        (a) => a.assetType === "precrawledArchive",
      );
      expect(precrawledAssets.length).toBe(1);
    });

    it("should support precrawling via singlefile with ifexists=overwrite-recrawl", async () => {
      // First upload
      const [data, response] = await uploadSinglefileAsset("overwrite-recrawl");
      expect(response?.status).toBe(201);
      const bookmarkId = data?.id;
      if (!bookmarkId) throw new Error("Bookmark ID not found");

      // Record the asset
      const { data: bookmark, response: getResponse1 } = await client.GET(
        "/bookmarks/{bookmarkId}",
        {
          params: { path: { bookmarkId } },
        },
      );
      expect(getResponse1.status).toBe(200);
      const firstAssetId = bookmark!.assets.find(
        (a) => a.assetType === "precrawledArchive",
      )?.id;
      expect(firstAssetId).toBeDefined();

      // Second upload with overwrite-recrawl
      const [data2, response2] =
        await uploadSinglefileAsset("overwrite-recrawl");
      expect(response2?.status).toBe(200);
      expect(data2?.id).toBe(bookmarkId);

      // Get the bookmark again
      const { data: bookmark2, response: getResponse2 } = await client.GET(
        "/bookmarks/{bookmarkId}",
        {
          params: { path: { bookmarkId } },
        },
      );
      expect(getResponse2.status).toBe(200);
      const secondAssetId = bookmark2!.assets.find(
        (a) => a.assetType === "precrawledArchive",
      )?.id;
      expect(secondAssetId).toBeDefined();
      expect(secondAssetId).not.toBe(firstAssetId);
      // There should be only one precrawledArchive asset
      const precrawledAssets = bookmark2!.assets.filter(
        (a) => a.assetType === "precrawledArchive",
      );
      expect(precrawledAssets.length).toBe(1);
    });

    it("should support precrawling via singlefile with ifexists=append", async () => {
      // First upload
      const [data, response] = await uploadSinglefileAsset("append");
      expect(response?.status).toBe(201);
      const bookmarkId = data?.id;
      if (!bookmarkId) throw new Error("Bookmark ID not found");

      // Record the first asset
      const { data: bookmark, response: getResponse1 } = await client.GET(
        "/bookmarks/{bookmarkId}",
        {
          params: { path: { bookmarkId } },
        },
      );
      expect(getResponse1.status).toBe(200);
      const firstAssetId = bookmark!.assets.find(
        (a) => a.assetType === "precrawledArchive",
      )?.id;
      expect(firstAssetId).toBeDefined();

      // Second upload with append
      const [data2, response2] = await uploadSinglefileAsset("append");
      expect(response2?.status).toBe(200);
      expect(data2?.id).toBe(bookmarkId);

      // Get the bookmark again
      const { data: bookmark2, response: getResponse2 } = await client.GET(
        "/bookmarks/{bookmarkId}",
        {
          params: { path: { bookmarkId } },
        },
      );
      expect(getResponse2.status).toBe(200);
      const precrawledAssets = bookmark2!.assets.filter(
        (a) => a.assetType === "precrawledArchive",
      );
      expect(precrawledAssets.length).toBe(2);
      expect(precrawledAssets.map((a) => a.id)).toContain(firstAssetId);
      // The second asset id should be different
      const secondAssetId = precrawledAssets.find(
        (asset) => asset.id !== firstAssetId,
      )?.id;
      expect(secondAssetId).toBeDefined();
    });

    it("should support precrawling via singlefile with ifexists=append-recrawl", async () => {
      // First upload
      const [data, response] = await uploadSinglefileAsset("append-recrawl");
      expect(response?.status).toBe(201);
      const bookmarkId = data?.id;
      if (!bookmarkId) throw new Error("Bookmark ID not found");

      // Record the first asset
      const { data: bookmark, response: getResponse1 } = await client.GET(
        "/bookmarks/{bookmarkId}",
        {
          params: { path: { bookmarkId } },
        },
      );
      expect(getResponse1.status).toBe(200);
      const firstAssetId = bookmark!.assets.find(
        (a) => a.assetType === "precrawledArchive",
      )?.id;
      expect(firstAssetId).toBeDefined();

      // Second upload with append-recrawl
      const [data2, response2] = await uploadSinglefileAsset("append-recrawl");
      expect(response2?.status).toBe(200);
      expect(data2?.id).toBe(bookmarkId);

      // Get the bookmark again
      const { data: bookmark2, response: getResponse2 } = await client.GET(
        "/bookmarks/{bookmarkId}",
        {
          params: { path: { bookmarkId } },
        },
      );
      expect(getResponse2.status).toBe(200);
      const precrawledAssets = bookmark2!.assets.filter(
        (a) => a.assetType === "precrawledArchive",
      );
      expect(precrawledAssets.length).toBe(2);
      expect(precrawledAssets.map((a) => a.id)).toContain(firstAssetId);
      // The second asset id should be different
      const secondAssetId = precrawledAssets.find(
        (asset) => asset.id !== firstAssetId,
      )?.id;
      expect(secondAssetId).toBeDefined();
    });
  });
});
